/**
 * @file ngtcpclient.cpp
 * @author Suk Robert Koh (srkoh87z@gmail.com)
 */

#include <stdio.h>
#include "ngine/client/ngtcpclient.h"

#ifdef WIN32
// function prototype
unsigned int WINAPI recvThread(LPVOID lpParam);
unsigned int WINAPI sendThread(LPVOID lpParam);
#endif

/**
 * Default constructor for NGTcpClient class
 *
 * @param	const char *	Address of the server to connect to
 * @param	int				Port number of the server to connect to
 */
NGTcpClient::NGTcpClient(const char *address, int port)
{
	// initialize socket and connect to server
	if(setUpNetwork(address, port))
	{
		/**
		 * TODO: handle error
		 */
	}

	// initialize variables
	recvBuffer = (char *) malloc(20960);
	bufferPosition = 0;

	// call worker threads
#ifndef WIN32
	pthread_t id1, id2;

	pthread_create(&id1, NULL, recvThread, (void *) this);
	pthread_create(&id2, NULL, sendThread, (void *) this);
#else
	unsigned int id1, id2;

	_beginthreadex(NULL, 0, recvThread, (LPVOID) this, 0, &id1);
	_beginthreadex(NULL, 0, sendThread, (LPVOID) this, 0, &id2);
#endif

	NGLogger::debug("NGTcpClient constructor\n");
}

/**
 * Destructor for NGTcpClient class
 */
NGTcpClient::~NGTcpClient()
{
	// clean up
	tearDownNetwork();
}

/**
 * Sets up the network connection with server
 *
 * @param	const char *	Address of the server to connect to
 * @param	int				Port number of the server to connect to
 * @return	BOOL			Whether or not it was successful
 */
BOOL NGTcpClient::setUpNetwork(const char *address, int port)
{
#ifdef WIN32
	WSADATA wsaData;

	// call WSAStartup
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		NGLogger::error("WSAStartup() failed");
		return FALSE;
	}
#endif

	// variables
	struct hostent *host;
	SOCKADDR_IN servAddr;

	host = gethostbyname(address);

	// create socket
	if((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		NGLogger::error("socket() error");
		return FALSE;
	}

	// fill in server information
	memset((char *) &servAddr, 0, sizeof(struct sockaddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_port = htons(port);
	servAddr.sin_addr = *((struct in_addr *) host->h_addr);

	// connect
	if(connect(serverSocket, (struct sockaddr *) &servAddr, sizeof(struct sockaddr)) == SOCKET_ERROR)
	{
		NGLogger::error("connect() error");
		return FALSE;
	}

	// set socket option for non-blocking
#ifdef WIN32
	char cFlag = 1;
	if(setsockopt(serverSocket, IPPROTO_TCP, TCP_NODELAY, &cFlag, sizeof(char)) == SOCKET_ERROR)
	{
		NGLogger::error("setsockopt() failed");
		return FALSE;
	}
#else
	int flags;

	flags = fcntl(serverSocket, F_GETFL, 0);
	fcntl(serverSocket, F_SETFL, flags | O_NONBLOCK);
#endif
}

/**
 * Tears down (cleans up) the network connection with server
 *
 * @return	BOOL	Whether or not it was successful
 */
BOOL NGTcpClient::tearDownNetwork()
{
#ifndef _WIN32
	close(serverSocket);
#else
	closesocket(serverSocket);
	WSACleanup();
#endif
}

#ifdef WIN32
/**
 * Runs in the background to call handleRecv() - for Windows
 *
 * @param	LPVOID		Pointer to this instance
 */
unsigned int WINAPI recvThread(LPVOID lpParam)
{
	NGTcpClient *tmp = (NGTcpClient *) lpParam;

	while(1)
	{
		tmp->handleRecv();
	}

	return (DWORD) lpParam;
}

/**
 * Runs in the background to call handleSend() - for Windows
 *
 * @param	LPVOID		Pointer to this instance
 */
unsigned int WINAPI sendThread(LPVOID lpParam)
{
	NGTcpClient *tmp = (NGTcpClient *) lpParam;

	while(1)
	{
		tmp->handleSend();
	}

	return (DWORD) lpParam;
}
#else
/**
 * Runs in the background and calls handleRecv()
 *
 * @param	void *		Pointer to this instance
 */
void *NGTcpClient::recvThread(void *arg)
{
	NGTcpClient *tmp = (NGTcpClient *) arg;

	while(1)
	{
		tmp->handleRecv();
	}
}

/**
 * Runs in the background and calls handleSend()
 *
 * @param	void *		Pointer to this instance
 */
void *NGTcpClient::sendThread(void *arg)
{
	NGTcpClient *tmp = (NGTcpClient *) arg;

	while(1)
	{
		tmp->handleSend();
	}
}
#endif

/**
 * Handles receiving message
 *
 * @return	BOOL	Whether or not it was successful
 */
BOOL NGTcpClient::handleRecv(void)
{
	char buf[DEFAULT_BUF_SIZE];
	int count;

	// recv
	memset(buf, 0, DEFAULT_BUF_SIZE);
	if((count = recv(serverSocket, buf, DEFAULT_BUF_SIZE, 0)) <= 0)
		return FALSE;
	else
	{
		// add to the recvBuffer
		// TODO: decide which one is more efficient: to check message first then put into recvBuffer
		//		 or put into recvBuffer first then check
		memcpy(recvBuffer + bufferPosition, buf, count);
		bufferPosition += count;

		char buffer[DEFAULT_BUF_SIZE];
		WORD length = 0;

		while(bufferPosition != 0)
		{
			memset(buffer, 0, DEFAULT_BUF_SIZE);
			memcpy(buffer, recvBuffer, messageCoder->getHeaderSize());
			length = messageCoder->getMessageLength(buffer);

			// check if entire message has been received (in terms of size)
			if(bufferPosition > length + messageCoder->getHeaderSize())
			{
				memcpy(buffer + messageCoder->getHeaderSize(), recvBuffer + messageCoder->getHeaderSize(), length + messageCoder->getTrailerSize());
				NGMessage *msg = messageCoder->decode(buffer, 0);

				if(msg == NULL)
				{
					/*
					 * TODO: log that user send in invalid message
					 * TODO: decide whether i'm going to kill the user connection - returning FALSE
					 * 		                or just ignore this message - returning TRUE
					 */
					return TRUE;
				}

				// message is valid, put it into queue
				recvQueue->push(msg);

				if(bufferPosition == strlen(msg->getData()) + messageCoder->getHeaderSize() + messageCoder->getTrailerSize())
					bufferPosition = 0;
				else
				{
					memcpy(recvBuffer, recvBuffer + strlen(buffer), bufferPosition - strlen(buffer));
					bufferPosition -= strlen(buffer);
					recvBuffer[bufferPosition] = '\0';
				}
			}
			else
				return TRUE;
		}

		memset(recvBuffer, 0, DEFAULT_RECV_BUF_SIZE);
		bufferPosition = 0;
	}

	return TRUE;
}

/**
 * Handles sending message
 *
 * @return	BOOL	Whether or not it was successful
 */
BOOL NGTcpClient::handleSend(void)
{
//	NGLogger::info("in handleSend");
	int count;

	// loops until all messages in the queue are sent
	while(sendQueue->size() > 0)
	{
		NGMessage *msg = sendQueue->peek();
		char *sendData = messageCoder->encode(msg);

		count = send(serverSocket, sendData, strlen(sendData), 0);

//		NGLogger::info("sent message: %d", count);

		if(count < 0)
			return FALSE;
		else if(count == strlen(sendData))
			sendQueue->remove();

		/**
		 * TODO: consider implementing maximum messages to be sent at each process()
		 */
	}

	return TRUE;
}
